package web.common.internal.jdo.engine;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import web.common.core.util.AssertUtil;
import web.common.core.util.StringUtil;
import web.common.internal.jdo.dialect.Dialect;
import web.common.internal.jdo.mapper.RowMapperFactory;
import web.common.internal.jdo.parser.EntityConfigCache;
import web.common.internal.jdo.parser.MapperConfigCache;
import web.common.internal.jdo.parser.ParserFactory;
import web.common.internal.jdo.sql.SqlHandlerFactory;
import web.common.internal.jdo.sql.SqlType;
import web.common.internal.jdo.util.IdUtil;
import web.common.internal.jdo.util.ObjectUtil;
import web.common.internal.jdo.util.ParamUtil;
import web.common.internal.jdo.util.VersionUtil;
import web.common.jdo.annotation.LockModeType;
import web.common.jdo.engine.Jdo;
import web.common.jdo.exception.JdbcException;
import web.common.jdo.exception.VersionException;
import web.common.jdo.parser.EntityConfig;
import web.common.jdo.parser.SqlItem;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Jin Zheng
 * @since 1.0 2022-01-12
 */
public class DefaultJdo implements Jdo {
    private static Logger logger = LoggerFactory.getLogger(DefaultJdo.class);

    private final Dialect dialect;
    private final EntityConfigCache entityConfigCache;
    private final MapperConfigCache mapperConfigCache;
    private final JdbcTemplate jdbcTemplate;
    private final SqlHandlerFactory sqlHandlerFactory;
    private final RowMapperFactory rowMapperFactory;

	public DefaultJdo(ParserFactory parserFactory, JdbcTemplate jdbcTemplate) {
        this.dialect = parserFactory.getDialect();
        this.entityConfigCache = parserFactory.getEntityConfigCache();
        this.mapperConfigCache = parserFactory.getMapperConfigCache();
        this.jdbcTemplate = jdbcTemplate;
        this.sqlHandlerFactory = new SqlHandlerFactory();
        this.rowMapperFactory = new RowMapperFactory(parserFactory);
    }

    @Override
    public int save(Object object, boolean dynamic) {
        AssertUtil.notNull(object, "Object");
        var config = entityConfigCache.get(object.getClass());
        var param = ObjectUtil.toMap(object, config, dynamic);
        IdUtil.generateId(param, config.getIdMap());
        var sqlItem = dynamic ? sqlHandlerFactory.handle(SqlType.INSERT, config, param) :
                config.getSqlConfig().getInsert();
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), dynamic);
        var creator = new DefaultPreparedStatementCreator(sql, paramList);
        KeyHolder holder = new GeneratedKeyHolder();
        int count = jdbcTemplate.update(creator, holder);
        IdUtil.setId(holder, object, config);
        return count;
    }

    @Override
    public int saveObject(Class<?> clazz, Map<String, ?> param) {
        var config = entityConfigCache.get(clazz);
        var map = ObjectUtil.generateMap(param, config);
        var sqlItem = sqlHandlerFactory.handle(SqlType.INSERT, config, map);
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        var paramList = ParamUtil.toParamList(map, sqlItem.getParamNameList(), false);
        return jdbcTemplate.update(sql, paramList.toArray());
    }

    @Override
    public int[] saveList(List<?> objectList) {
        AssertUtil.notEmpty(objectList, "ObjectList");
        var config = entityConfigCache.get(objectList.get(0).getClass());
        var sqlItem = config.getSqlConfig().getInsert();
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        List<Object[]> batchList = new ArrayList<>();
        for (var object : objectList) {
            var param = ObjectUtil.toMap(object, config, false);
            IdUtil.generateId(param, config.getIdMap());
            var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), false);
            batchList.add(paramList.toArray());
        }
        //KeyHolder holder = new GeneratedKeyHolder();
        int[] count = jdbcTemplate.batchUpdate(sql, batchList);
        //IdUtil.setId(holder, objectList, config);
        return count;
    }

    @Override
    public int update(Object object, boolean dynamic) {
        AssertUtil.notNull(object, "Object");
        var config = entityConfigCache.get(object.getClass());
        var param = ObjectUtil.toMap(object, config, dynamic);
        var sqlConfig = config.getSqlConfig();
        var isForceVer = VersionUtil.isForce(config);
        SqlItem sqlItem = null;
        if (dynamic) {
            sqlItem = sqlHandlerFactory.handle(SqlType.UPDATE, config, param);
        } else if (isForceVer) {
            sqlItem = sqlConfig.getUpdateByIdAndVersion();
        } else {
            sqlItem = sqlConfig.getUpdateById();
        }
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), dynamic);
        int count = jdbcTemplate.update(sql, paramList.toArray());
        if (isForceVer && count <= 0) {
            throw new VersionException(object.getClass());
        }
        return count;
    }

    @Override
    public int updateObject(Class<?> clazz, Object id, Map<String, ?> param) {
        var config = entityConfigCache.get(clazz);
        var idConfig = IdUtil.checkSingleId(config);
        Map<String, Object> map = new HashMap<>(param);
        map.put(idConfig.getFieldName(), id);
        var sqlItem = sqlHandlerFactory.handle(SqlType.UPDATE, config, map);
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        var isForceVer = VersionUtil.isForce(config);
        var paramList = ParamUtil.toParamList(map, sqlItem.getParamNameList(), false);
        int count = jdbcTemplate.update(sql, paramList.toArray());
        if (isForceVer && count <= 0) {
            throw new VersionException(clazz);
        }
        return count;
    }

    @Override
    public int[] updateList(List<?> objectList) {
        AssertUtil.notEmpty(objectList, "ObjectList");
        var clazz = objectList.get(0).getClass();
        var config = entityConfigCache.get(clazz);
        var sqlConfig = config.getSqlConfig();
        var isForceVer = VersionUtil.isForce(config);
        var sqlItem = isForceVer ? sqlConfig.getUpdateByIdAndVersion() : sqlConfig.getUpdateById();
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        List<Object[]> batchList = new ArrayList<>();
        for (var object : objectList) {
            var param = ObjectUtil.toMap(object, config, false);
            IdUtil.generateId(param, config.getIdMap());
            var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), false);
            batchList.add(paramList.toArray());
        }
        int[] count = jdbcTemplate.batchUpdate(sql, batchList);
        if (isForceVer) {
            for (int c : count) {
                if (c <= 0) {
                    throw new VersionException(clazz);
                }
            }
        }
        return count;
    }

    @Override
    public int inc(Class<?> clazz, Object id, Map<String, ? extends Number> param) {
        var config = entityConfigCache.get(clazz);
        var idConfig = IdUtil.checkSingleId(config);
        var sqlItem = sqlHandlerFactory.handle(SqlType.INC, config, param);
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        Map<String, Object> map = new HashMap<>(param);
        map.put(idConfig.getFieldName(), id);
        var paramList = ParamUtil.toParamList(map, sqlItem.getParamNameList(), false);
        return jdbcTemplate.update(sql, paramList.toArray());
    }

    @Override
    public int deleteId(Class<?> clazz, Object id) {
        var config = entityConfigCache.get(clazz);
        IdUtil.checkSingleId(config);
        var sqlItem = config.getSqlConfig().getDeleteById();
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        return jdbcTemplate.update(sql, id);
    }

    @Override
    public <K, T> int deleteIdList(Class<T> clazz, List<K> idList) {
        var config = entityConfigCache.get(clazz);
        var id = IdUtil.checkSingleId(config);
        var sqlItem = config.getSqlConfig().getDeleteByIdList();
        var sql = String.format(sqlItem.getSql(), StringUtil.repeat("?", idList.size(), ","));
        this.logSQL(sql);
        return jdbcTemplate.update(sql, idList.toArray());
    }

    @Override
    public <T> int deleteBy(Class<T> clazz, Map<String, ?> param) {
        var config = entityConfigCache.get(clazz);
        var sqlItem = sqlHandlerFactory.handle(SqlType.DELETE, config, param);
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), false);
        return jdbcTemplate.update(sql, paramList.toArray());
    }

    @Override
    public <T> T get(Class<T> clazz, Object id, LockModeType type) {
        var config = entityConfigCache.get(clazz);
        IdUtil.checkSingleId(config);
        var sqlItem = config.getSqlConfig().getSelectById();
        var sql = dialect.lock(dialect.page(sqlItem.getSql(), 0, 1), type);
        this.logSQL(sql);
        var mapper = rowMapperFactory.getRowMapper(clazz);
        List<T> list = jdbcTemplate.query(sql, mapper, id);
        if (list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }

    @Override
    public <K, T> Map<K, T> getList(Class<T> clazz, List<K> idList) {
        var config = entityConfigCache.get(clazz);
        var id = IdUtil.checkSingleId(config);
        var sqlItem = config.getSqlConfig().getSelectByIdList();
        var sql = String.format(sqlItem.getSql(), StringUtil.repeat("?", idList.size(), ","));
        this.logSQL(sql);
        var mapper = rowMapperFactory.getRowMapper(clazz);
        List<T> list = jdbcTemplate.query(sql, mapper, idList.toArray());
        if (list.isEmpty()) {
            return Map.of();
        }

        Map<K, T> map = new HashMap<>();
        var idField = id.getBeanField();
        for (var o : list) {
            map.put((K)idField.getFieldValue(o), o);
        }
        return map;
    }

    @Override
    public <T> T getField(Class<?> clazz, Class<T> target, String field, Map<String, ?> param) {
        var config = entityConfigCache.get(clazz);
        var sqlItem = sqlHandlerFactory.handle(SqlType.GET_FIELD, config, field, param);
        var sql = dialect.getOne(sqlItem.getSql());
        this.logSQL(sql);
        var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), false);
        List<T> list = jdbcTemplate.queryForList(sql, target, paramList.toArray());
        if (list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }

    @Override
    public <T> T getObject(Class<T> clazz, Map<String, ?> param) {
        var config = entityConfigCache.get(clazz);
        var sqlItem = sqlHandlerFactory.handle(SqlType.GET, config, param);
        var sql = dialect.getOne(sqlItem.getSql());
        this.logSQL(sql);
        var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), false);
        var mapper = rowMapperFactory.getRowMapper(clazz);
        List<T> list = jdbcTemplate.query(sql, mapper, paramList.toArray());
        if (list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }

    @Override
    public boolean exist(Object object, String... names) {
        AssertUtil.notNull(object, "Object");
        var nameList = Arrays.asList(names);
        var config = entityConfigCache.get(object.getClass());
        var param = ObjectUtil.toMap(object, config, false);
        var sqlItem = sqlHandlerFactory.handle(SqlType.EXIST, config, param, nameList);
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), false);
        List<Integer> list = jdbcTemplate.queryForList(sql, Integer.class, paramList.toArray());
        if (list.isEmpty()) {
            return false;
        }
        int count = list.get(0);
        return count > 0;
    }

    @Override
    public int count(Class<?> clazz, Map<String, ?> param) {
        var config = entityConfigCache.get(clazz);
        var sqlItem = sqlHandlerFactory.handle(SqlType.COUNT, config, param);
        var sql = sqlItem.getSql();
        this.logSQL(sql);
        var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), false);
        List<Integer> list = jdbcTemplate.queryForList(sql, Integer.class, paramList.toArray());
        if (list.isEmpty()) {
            logger.warn("No result");
            return -1;
        }
        return list.get(0);
    }

    @Override
    public <T> T getObject(Class<T> clazz, String sql, List<?> paramList) {
        this.logSQL(sql);
        var mapper = rowMapperFactory.getRowMapper(clazz);
        var params = paramList == null ? new Object[0] : ParamUtil.convert(paramList).toArray();
        List<T> list = jdbcTemplate.query(sql, mapper, params);
        if (list.isEmpty()) {
            throw null;
        }
        return list.get(0);
    }

    @Override
    public <T> List<T> list(Class<T> clazz, String sql, int start, int size) {
        sql = dialect.page(sql, start, size);
        return this.list(clazz, sql, null);
    }

    @Override
    public <T> List<T> list(Class<T> clazz, String sql, List<?> paramList, int start, int size) {
        sql = dialect.page(sql, start, size);
        return this.list(clazz, sql, paramList);
    }

    @Override
    public <T> List<T> list(Class<T> clazz, String sql, List<?> paramList) {
        this.logSQL(sql);
        var mapper = rowMapperFactory.getRowMapper(clazz);
        var params = paramList == null ? new Object[0] : ParamUtil.convert(paramList).toArray();
        return jdbcTemplate.query(sql, mapper, params);
    }

    @Override
    public <T> List<T> listField(Class<?> clazz, Class<T> target, String field, Map<String, ?> param) {
        var config = entityConfigCache.get(clazz);
        var sqlItem = sqlHandlerFactory.handle(SqlType.GET_FIELD, config, field, param);
        var sql = dialect.getOne(sqlItem.getSql());
        this.logSQL(sql);
        var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), false);
        return jdbcTemplate.queryForList(sql, target, paramList.toArray());
    }

    @Override
    public <T> List<T> listObject(Class<T> clazz, Map<String, ?> param) {
        var config = entityConfigCache.get(clazz);
        var sqlItem = sqlHandlerFactory.handle(SqlType.GET, config, param);
        var sql = dialect.getOne(sqlItem.getSql());
        this.logSQL(sql);
        var paramList = ParamUtil.toParamList(param, sqlItem.getParamNameList(), false);
        var mapper = rowMapperFactory.getRowMapper(clazz);
        return jdbcTemplate.query(sql, mapper, paramList.toArray());
    }

    @Override
    public void execute(String sql) {
        this.logSQL(sql);
        jdbcTemplate.execute(sql);
    }

    @Override
    public EntityConfig checkEntityConfig(Class<?> clazz) {
        boolean existEntity = entityConfigCache.exist(clazz);
        boolean existMapper = mapperConfigCache.exist(clazz);
        if (!existEntity) {
            if (existMapper) {
                throw new JdbcException(clazz.getName() + " 需要覆写 select(), selectCount() 和 orderBy() 方法");
            }
            else {
                throw new JdbcException(clazz.getName() + " 缺少 @Entity 或 @Mapper 注解");
            }
        }
        return entityConfigCache.get(clazz);
    }

    private void logSQL(String sql) {
        if (logger.isDebugEnabled()) {
            logger.debug("SQL: {}", sql);
        }
    }
}
